Skip to content

[SYSTEMDS-3863] Add PowerTransformer built-in functions - #2499

Open
WenliangCao wants to merge 14 commits into
apache:mainfrom
WenliangCao:systemds-3863-power-transformer
Open

[SYSTEMDS-3863] Add PowerTransformer built-in functions#2499
WenliangCao wants to merge 14 commits into
apache:mainfrom
WenliangCao:systemds-3863-power-transformer

Conversation

@WenliangCao

@WenliangCao WenliangCao commented Jun 21, 2026

Copy link
Copy Markdown

Summary

This pull request adds PowerTransformer built-ins with separate fit and apply workflows. The implementation supports Yeo-Johnson and Box-Cox transformations, per-column parameter estimation, optional standardization, and reuse of fitted parameters on new data.

Yeo-Johnson is the default method and supports mixed-sign inputs. Box-Cox is available for strictly positive inputs.

API

[Y, lambdas, means, scales] = powerTransform(
    X,
    method="yeo-johnson",
    standardize=TRUE
)

Y = powerTransformApply(
    X,
    lambdas,
    means,
    scales,
    method="yeo-johnson"
)

powerTransform estimates one lambda per column and optionally standardizes the transformed columns. powerTransformApply reuses the fitted lambdas, means, and scales without re-estimation.

Implementation

  • Add powerTransform.dml for fitting and transforming feature matrices.
  • Add powerTransformApply.dml for applying fitted transformations.
  • Support Yeo-Johnson and Box-Cox transformations.
  • Keep Yeo-Johnson as the default method.
  • Support optional zero-mean, unit-variance standardization.
  • Estimate each lambda by minimizing the negative log-likelihood with Brent optimization.
  • Treat [-2, 2] as an initial bracket and expand it when necessary, allowing the optimum to lie outside the initial interval.
  • Validate method names, fitted-state dimensions, and Box-Cox input requirements.
  • Register both functions as SystemDS built-ins.
  • Add entries for both functions to the built-in reference documentation.

Validation

mvn -Dtest=BuiltinPowerTransformTest test
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

The tests cover:

  • default Yeo-Johnson fitting with mixed-sign and constant columns;
  • Box-Cox fitting without standardization;
  • Yeo-Johnson and Box-Cox optima outside the initial [-2, 2] interval;
  • rejection of non-positive Box-Cox input;
  • fitted Yeo-Johnson and Box-Cox apply workflows; and
  • standardization state reuse.

Numerical outputs are compared with independent R reference implementations.

Documentation

User-facing signatures, arguments, return values, input requirements, and examples are documented in docs/site/builtins-reference.md.

Experiments

The reproducible evaluation compares no scaling, standard scaling, robust scaling, Yeo-Johnson, and Box-Cox on nine public datasets. Each experiment uses the fixed seeds 13, 37, 73, 101, and 149.

Task Estimator Primary metric
Regression 5-nearest-neighbors regressor RMSE (lower is better)
Classification 5-nearest-neighbors classifier Macro-F1 (higher is better)
Clustering K-means (n_init=20) ARI (higher is better)

Supervised tasks use 80/20 train/test splits; classification is stratified and Parkinsons Telemonitoring uses subject-level group splits. Each transformation is fitted on training data and applied to test data. Clustering uses the full dataset.

The table reports the mean primary metric over five runs. A dash indicates that Box-Cox is not applicable because the feature matrix contains non-positive values.

Task Dataset Metric None Standard Robust Yeo-Johnson Box-Cox
Regression California Housing RMSE 1.0607 0.6486 0.6460 0.6114
Regression Abalone RMSE 2.2966 2.3575 2.3673 2.3547 2.3580
Regression Parkinsons Telemonitoring RMSE 12.8276 13.0932 12.6718 12.7222
Classification Breast Cancer Wisconsin Macro-F1 0.9303 0.9559 0.9560 0.9618
Classification Wine Macro-F1 0.6448 0.9734 0.9629 0.9683 0.9683
Classification HTRU2 Macro-F1 0.9116 0.9312 0.9339 0.9327
Clustering Iris ARI 0.7302 0.6201 0.5803 0.6410 0.6180
Clustering Seeds ARI 0.7166 0.7733 0.7722 0.7759 0.7759
Clustering Wholesale Customers ARI -0.0306 0.1873 -0.0024 0.5264 0.5330

Power transformations rank first or tie for first on four of the nine datasets. Representative results include:

  • California Housing: Yeo-Johnson reduces RMSE from 0.6486 with standard scaling to 0.6114.
  • Breast Cancer Wisconsin: Yeo-Johnson achieves the highest Macro-F1 at 0.9618.
  • Seeds: Yeo-Johnson and Box-Cox tie for the highest ARI at 0.7759.
  • Wholesale Customers: Box-Cox achieves an ARI of 0.5330, compared with 0.1873 for standard scaling.

PowerTransformer experiment leaderboard

Reproducible artifacts

Limitations

The downstream effect of preprocessing is dataset- and estimator-dependent. The evaluation uses fixed distance-based estimators, three datasets per task, and no hyperparameter search or statistical significance test. The results therefore demonstrate practical cases where power transformations help, rather than universal superiority over other preprocessing methods.

Related issue

SYSTEMDS-3863

@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.65%. Comparing base (af500c8) to head (a9cde52).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #2499      +/-   ##
============================================
+ Coverage     71.61%   71.65%   +0.04%     
- Complexity    50352    50490     +138     
============================================
  Files          1626     1628       +2     
  Lines        194664   195069     +405     
  Branches      38007    38042      +35     
============================================
+ Hits         139408   139782     +374     
- Misses        44328    44364      +36     
+ Partials      10928    10923       -5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Add two DML builtins for the midterm PowerTransformer prototype:
- powerTransform.dml estimates one Yeo-Johnson lambda per input column,
  uses lambda = 1 for constant columns, and applies the transform.
- powerTransformApply.dml applies the Yeo-Johnson transform with supplied
  per-column lambdas.

Register both builtins so they can be resolved by SystemDS.

Add powerTransformSmokeTest.dml to verify:
- powerTransformApply with lambda = 1 behaves as identity
- powerTransform returns output dimensions matching the input
- one lambda is returned per input column
- constant columns use lambda = 1
- transformed output and lambdas do not contain NaN or Inf

The smoke test passes with:
./bin/systemds src/test/scripts/functions/builtin/powerTransformSmokeTest.dml
Add a focused PowerTransformApply test that compares the DML builtin against
an independent R reference implementation.

The test adds:
- powerTransformApply.dml as a small DML wrapper around the registered builtin
- powerTransformApply.R as the reference Yeo-Johnson apply implementation
- BuiltinPowerTransformTest.java to run the DML and R scripts and compare Y

The test covers the key apply branches with fixed lambdas:
- lambda = 0 for the positive log branch
- lambda = 1 for the identity-style middle case
- lambda = 2 for the negative log branch

Also translate the remaining PowerTransformer comments from Chinese to English
in the prototype DML files.

Verified with:
Rscript -e 'parse(file="src/test/scripts/functions/builtin/powerTransformApply.R"); cat("R syntax OK\n")'
./bin/systemds src/test/scripts/functions/builtin/powerTransformSmokeTest.dml
mvn -Dtest=BuiltinPowerTransformTest test
@WenliangCao
WenliangCao force-pushed the systemds-3863-power-transformer branch from 130c741 to 753c5a0 Compare August 2, 2026 21:16

@gaturchenko gaturchenko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @WenliangCao, thank you very much for the contribution and extensive experiments. I have left some comments, please address them so that we can merge your code into the main. The main issue however is a failing test, we cannot merge if the CI is red, so please address it first. Feel free to reach out via an email or right here in the PR thread if anything is unclear or if you disagree with some of my comments, we can absolutely discuss anything.

Comment thread scripts/builtin/powerTransform.dml
Comment thread scripts/builtin/powerTransformApply.dml
Comment thread scripts/builtin/powerTransform.dml Outdated

# Apply one transform with this lambda and use the temporary y for scoring
emptyStats = matrix(0.0, rows=0, cols=0)
y = powerTransformApply(x, lambdaMatrix, emptyStats, emptyStats, method);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has a fixed invocation overhead, and for matrices with, say, 1000 rows and 4 columns, this will dominate the runtime, so it should be inlined somehow

Comment thread scripts/builtin/powerTransform.dml Outdated

# Jacobian term for the selected transformation
if (method == "box-cox") {
jacobian = (lambda - 1.0) * sum(log(x));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only lambda changes here, x is always the same, so you can compute sum(log(x)) just once, which can be done with a separate function. Then you can add something like jacTerm as another input to ptNegLogLikelihood and get jacobian = (lambda - 1.0) * jacTerm;. This should improve performance substantially for large matrices

Comment thread scripts/builtin/powerTransform.dml Outdated
jacobian = (lambda - 1.0) * sum(log(x));
}
else {
jacobian = (lambda - 1.0) * sum(sign(x) * log(abs(x) + 1.0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same logic as for the comment above


private void runPowerTransformTest(
String method, boolean standardize, double[][] input, boolean shouldFail) {
ExecMode oldExecMode = setExecMode(ExecType.CP);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only add CP coverage, would be good to have Spark covered as well. Let us know if there are particular issues with Spark for Power Transform however

}

@Test
public void testPowerTransformYeoJohnsonLambdaAboveInitialInterval() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test fails both in CI and for me locally

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementing powerTransform like this defeats the purpose of R tests, as we want to compare against an actual existing R implementation. A better way of doing this is to actually find an R package that implements powerTransform with your 2 methods and compare against it

Comment thread src/test/scripts/functions/builtin/powerTransformApply.R Outdated
@github-project-automation github-project-automation Bot moved this from In Progress to In Review in SystemDS PR Queue Aug 3, 2026
Add the required blank line after the Apache license headers in the PowerTransformer fit and apply built-ins.

Remove the redundant -exec singlenode arguments from the fit and apply tests, which already select the execution mode through the test framework.

Apply dev/format-changed.sh against upstream/main to format PR-edited Java lines with dev/CodeStyle_eclipse.xml and address the Java Format Check failure.
@gaturchenko

Copy link
Copy Markdown
Contributor

In addition the format check fails @WenliangCao, please run dev/format-changed.sh from SystemDS root to fix

@WenliangCao
WenliangCao force-pushed the systemds-3863-power-transformer branch from a9cde52 to 5df732f Compare August 4, 2026 14:43
@WenliangCao
WenliangCao force-pushed the systemds-3863-power-transformer branch from 5df732f to 280e878 Compare August 4, 2026 14:54
@WenliangCao
WenliangCao requested a review from gaturchenko August 5, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

2 participants